﻿#region Usings

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Xml;
using System.Xml.Serialization;

using Gonte.Xml.Serialization; 

#endregion

namespace Gonte.Logging
{
    /// <summary>
    /// Logs messages
    /// </summary>
    public class Logger
    {
        #region Constants

        public enum LogLevel
        {
            Off = 0,
            Information = 1,
            Special = 2,
            Warning = 4,
            Error = 8,
            All = Information | Special | Warning | Error
        }

        private const string DefaultLoggerName = "*";

        #endregion

        #region Constructors

        static Logger()
        {
            configuration_ = new LoggingConfiguration();
            LoggingConfigurationSection loggingSection = (LoggingConfigurationSection)ConfigurationManager.GetSection(LoggingConfigurationSection.LoggerConfigurationSectionString);
            if (loggingSection != null)
            {
                string filePath = PathMapper.MapPath(loggingSection.ConfigurationFile);
                if (!string.IsNullOrEmpty(filePath) // There is a file path
                    && File.Exists(filePath))       // and the file exists
                {
                    XmlObjectDeserializer deserializer = XmlObjectDeserializer.GetDeserializer(configuration_);
                    deserializer.Deserialize(filePath);
                }

                // TODO: Retrieve the embedded collection of loggers directly from the configuration file and add it to the loggers
            }
            else // Create a default logger that allows to log all the logs and all the levels
            {
                Logger logger = new Logger(DefaultLoggerName);
                logger.level_ = LogLevel.All;
                configuration_.Loggers.Add(DefaultLoggerName, logger);
            }
        }

        public Logger() // Needed by the serializer
        {
        }

        private Logger(string name)
        {
            name_ = name;
        } 

        #endregion

        #region Static Methods

        public static Logger GetLogger(string name)
        {
            Dictionary<string, Logger> loggers = configuration_.Loggers;

            if (loggers.ContainsKey(DefaultLoggerName)) // Return the default logger if exists
            {
                return loggers[DefaultLoggerName];
            }

            lock (lock_)
            {
                if (!loggers.ContainsKey(name)) // Create a new log for the object
                {
                    Logger logger = new Logger(name);
                    loggers[name] = logger;
                }

                return loggers[name];
            }
        }

        #region Automatic type detector

        #region Simple message logger

        /// <summary>
        /// Logs a fixed message
        /// </summary>
        /// <param name="level"></param>
        /// <param name="message"></param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void Log(LogLevel level, string message)
        {
            StackFrame stackFrame = new StackFrame(1, false);
            Logger logger = GetLogger(stackFrame.GetMethod().DeclaringType.FullName);

            if (logger.CanLog(level))
            {
                logger.Write(level, message);
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void LogInformation(string message)
        {
            StackFrame stackFrame = new StackFrame(1, false);
            Logger logger = GetLogger(stackFrame.GetMethod().DeclaringType.FullName);

            if (logger.CanLog(LogLevel.Information))
            {
                logger.Write(LogLevel.Information, message);
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void LogWarning(string message)
        {
            StackFrame stackFrame = new StackFrame(1, false);
            Logger logger = GetLogger(stackFrame.GetMethod().DeclaringType.FullName);

            if (logger.CanLog(LogLevel.Warning))
            {
                logger.Write(LogLevel.Warning, message);
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void LogError(string message)
        {
            StackFrame stackFrame = new StackFrame(1, false);
            Logger logger = GetLogger(stackFrame.GetMethod().DeclaringType.FullName);

            if (logger.CanLog(LogLevel.Error))
            {
                logger.Write(LogLevel.Error, message);
            }
        } 

        #endregion

        #region Logger with formatting

        /// <summary>
        /// Logs with formatting
        /// </summary>
        /// <param name="level"></param>
        /// <param name="format"></param>
        /// <param name="args"></param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void Log(LogLevel level, string format, params Object[] args)
        {
            StackFrame stackFrame = new StackFrame(1, false);
            Logger logger = GetLogger(stackFrame.GetMethod().DeclaringType.FullName);

            if (logger.CanLog(level)) // Verify we can log before calling string.Format
            {
                logger.Write(level, string.Format(format, args));
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void LogInformation(string format, params Object[] args)
        {
            StackFrame stackFrame = new StackFrame(1, false);
            Logger logger = GetLogger(stackFrame.GetMethod().DeclaringType.FullName);

            if (logger.CanLog(LogLevel.Information)) // Verify we can log before calling string.Format
            {
                logger.Write(LogLevel.Information, string.Format(format, args));
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void LogWarning(string format, params Object[] args)
        {
            StackFrame stackFrame = new StackFrame(1, false);
            Logger logger = GetLogger(stackFrame.GetMethod().DeclaringType.FullName);

            if (logger.CanLog(LogLevel.Warning)) // Verify we can log before calling string.Format
            {
                logger.Write(LogLevel.Warning, string.Format(format, args));
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void LogError(string format, params Object[] args)
        {
            StackFrame stackFrame = new StackFrame(1, false);
            Logger logger = GetLogger(stackFrame.GetMethod().DeclaringType.FullName);

            if (logger.CanLog(LogLevel.Error)) // Verify we can log before calling string.Format
            {
                logger.Write(LogLevel.Error, string.Format(format, args));
            }
        }

        #endregion

        #endregion

        #region Explicit type

        #region Simple message logger

        /// <summary>
        /// Logs a fixed message
        /// </summary>
        /// <param name="level"></param>
        /// <param name="message"></param>
        public static void Log(Type type, LogLevel level, string message)
        {
            Logger logger = GetLogger(type.FullName);

            if (logger.CanLog(level))
            {
                logger.Write(level, message);
            }
        }

        public static void LogInformation(Type type, string message)
        {
            Logger logger = GetLogger(type.FullName);

            if (logger.CanLog(LogLevel.Information))
            {
                logger.Write(LogLevel.Information, message);
            }
        }

        public static void LogWarning(Type type, string message)
        {
            Logger logger = GetLogger(type.FullName);

            if (logger.CanLog(LogLevel.Warning))
            {
                logger.Write(LogLevel.Warning, message);
            }
        }

        public static void LogError(Type type, string message)
        {
            Logger logger = GetLogger(type.FullName);

            if (logger.CanLog(LogLevel.Error))
            {
                logger.Write(LogLevel.Error, message);
            }
        }

        #endregion

        #region Logger with formatting

        public static void Log(Type type, LogLevel level, string format, params Object[] args)
        {
            Logger logger = GetLogger(type.FullName);

            if (logger.CanLog(level)) // Verify we can log before calling string.Format
            {
                logger.Write(level, string.Format(format, args));
            }
        }

        public static void LogInformation(Type type, string format, params Object[] args)
        {
            Logger logger = GetLogger(type.FullName);

            if (logger.CanLog(LogLevel.Information)) // Verify we can log before calling string.Format
            {
                logger.Write(LogLevel.Information, string.Format(format, args));
            }
        }

        public static void LogWarning(Type type, string format, params Object[] args)
        {
            Logger logger = GetLogger(type.FullName);

            if (logger.CanLog(LogLevel.Warning)) // Verify we can log before calling string.Format
            {
                logger.Write(LogLevel.Warning, string.Format(format, args));
            }
        }

        public static void LogError(Type type, string format, params Object[] args)
        {
            Logger logger = GetLogger(type.FullName);

            if (logger.CanLog(LogLevel.Error)) // Verify we can log before calling string.Format
            {
                logger.Write(LogLevel.Error, string.Format(format, args));
            }
        } 

        #endregion

        #endregion

        #endregion

        #region Methods

        /// <summary>
        /// Writes a log
        /// </summary>
        /// <param name="level"></param>
        /// <param name="message"></param>
        public void Write(LogLevel level, string message)
        {
            foreach (TraceListener listener in Listeners)
            {
                string msg = string.Format("[{0}] - {1}", level.ToString(), message);

                listener.WriteLine(msg);
                listener.Flush();
            }
        }

        #endregion

        #region Properties

        public string Name
        {
            get { return name_; }
            set { name_ = value; }
        }

        [XmlIgnore()]
        public LogLevel Level
        {
            get { return level_; }
            set { level_ = value; }
        }

        [XmlAttribute("LogLevel")]
        public string LevelString
        {
            get { return level_.ToString(); }

            set 
            {
                switch (value.Trim().ToLower())
                {
                    case "off":
                        {
                            level_ = LogLevel.Off;
                        }
                        break;
                    case "info":
                    case "information":
                        {
                            level_ = LogLevel.Information;
                        }
                        break;
                    case "special":
                        {
                            level_ = LogLevel.Special;
                        }
                        break;
                    case "warn":
                    case "warning":
                        {
                            level_ = LogLevel.Warning;
                        }
                        break;
                    case "error":
                        {
                            level_ = LogLevel.Error;
                        }
                        break;
                    case "all":
                    case "any":
                        {
                            level_ = LogLevel.All;
                        }
                        break;
                    default:
                        {
                            throw new ArgumentException(string.Format("Invalid log level: {0}", value));
                        }
                }
            }
        }

        [XmlIgnore()]
        public List<TraceListener> Listeners
        {
            get 
            {
                lock (lock_)
                {
                    if (listeners_.Count == 0)
                    {
                        foreach (string name in tracers_)
                        {
                            listeners_.Add(configuration_.Listeners[name]);
                        }
                    }

                    if (listeners_.Count == 0) // If there are no listeners in the configuration then set the default listener if there are none
                    {
                        listeners_.Add(configuration_.GetDefaultListener());
                    }
                }
                
                return listeners_; 
            }

            set { listeners_ = value; }
        }

        /// <summary>
        /// The name of the tracers or listeners of this logger
        /// </summary>
        [XmlElement("Tracer")]
        public List<string> Tracers
        {
            get { return tracers_; }
            set { tracers_ = value; }
        }

        [XmlIgnore()]
        public static LoggingConfiguration Configuration
        {
            get { return Logger.configuration_; }
        }

        #endregion

        #region Helpers

        /// <summary>
        /// Determine whether the logger can log
        /// </summary>
        /// <param name="level"></param>
        /// <returns>True if the logger can log, false otherwise</returns>
        private bool CanLog(LogLevel level)
        {
            if (level == LogLevel.Off
                || level == LogLevel.All)
            {
                throw new ArgumentException("Log level must be either: Information, Warning or Error", "Log level");
            }

            switch (level_)
            {
                case LogLevel.Off: return false;
                case LogLevel.All: return true;
                default: return (level_ == level);
            }
        }

        #endregion

        #region Fields

        private string name_;
        private LogLevel level_ = LogLevel.Off;
        private List<TraceListener> listeners_ = new List<TraceListener>();
        private List<string> tracers_ = new List<string>();

        private static readonly LoggingConfiguration configuration_;
        private static readonly Object lock_ = new Object();

        #endregion
    }
}
